Enumの強力な代替手段、const assertionsとunion typesを解説。堅牢で保守性の高いコードに最適な使い方を学びましょう。
Enumを超えて:TypeScript Const Assertions vs. Union Types
TypeScriptによる静的型付けのJavaScriptの世界では、enum は、固定された名前付き定数のセットを表すための定番の方法として長く使われてきました。関連する値のコレクションを明確で読みやすい方法で定義できます。しかし、プロジェクトが成長し、進化するにつれて、開発者はより柔軟で、場合によってはよりパフォーマンスの高い代替手段を求めることがよくあります。よく登場する2つの強力な候補は、const assertions と union types です。この記事では、従来のenumに対するこれらの代替手段の使用方法について詳しく掘り下げ、実践的な例を示し、どちらを選択すべきかを説明します。
従来のTypeScript Enumの理解
代替手段を探求する前に、標準的なTypeScript enumの仕組みをしっかりと理解しておくことが不可欠です。Enumを使用すると、名前付きの数値または文字列定数のセットを定義できます。数値 (デフォルト) または文字列ベースにすることができます。
数値Enum
デフォルトでは、enumメンバーには0から始まる数値が割り当てられます。
enum DirectionNumeric {
Up,
Down,
Left,
Right
}
let myDirection: DirectionNumeric = DirectionNumeric.Up;
console.log(myDirection); // Output: 0
明示的に数値値を割り当てることもできます。
enum StatusCode {
Success = 200,
NotFound = 404,
InternalError = 500
}
let responseStatus: StatusCode = StatusCode.Success;
console.log(responseStatus); // Output: 200
文字列Enum
コンパイルされたJavaScriptでメンバー名が保持されるため、文字列enumはデバッグエクスペリエンスが向上するため、多くの場合好まれます。
enum ColorString {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}
let favoriteColor: ColorString = ColorString.Blue;
console.log(favoriteColor); // Output: "BLUE"
Enumのオーバーヘッド
enumは便利ですが、わずかなオーバーヘッドが伴います。JavaScriptにコンパイルされると、TypeScript enumは、多くの場合、逆マッピング (たとえば、数値値をenum名にマッピングし直す) を持つオブジェクトに変換されます。これは役立ちますが、バンドルサイズにも影響し、必ずしも必要とは限りません。
このシンプルな文字列enumを考えてみましょう。
enum Status {
Pending = "PENDING",
Processing = "PROCESSING",
Completed = "COMPLETED"
}
JavaScriptでは、次のようになる可能性があります。
var Status;
(function (Status) {
Status["Pending"] = "PENDING";
Status["Processing"] = "PROCESSING";
Status["Completed"] = "COMPLETED";
})(Status || (Status = {}));
単純な読み取り専用の定数のセットの場合、この生成されたコードは少し過剰に感じる可能性があります。
代替案1:Const Assertions
Const assertionsは、値に対して可能な限り最も具体的な型をコンパイラーに推論させる強力なTypeScript機能です。固定された値のセットを表すことを目的とした配列またはオブジェクトで使用すると、enumの軽量な代替手段として機能します。
配列でのConst Assertions
文字列リテラルの配列を作成し、const assertionを使用して、その型を不変にし、その要素をリテラル型にすることができます。
const statusArray = ["PENDING", "PROCESSING", "COMPLETED"] as const;
type StatusType = typeof statusArray[number];
let currentStatus: StatusType = "PROCESSING";
// currentStatus = "FAILED"; // Error: Type '"FAILED"' is not assignable to type 'StatusType'.
function processStatus(status: StatusType) {
console.log(`Processing status: ${status}`);
}
processStatus("COMPLETED");
ここで何が起こっているのかを見てみましょう。
as const:このassertionは、TypeScriptに配列を読み取り専用として扱い、その要素に対して最も具体的なリテラル型を推論するように指示します。したがって、string[]の代わりに、型はreadonly ["PENDING", "PROCESSING", "COMPLETED"]になります。typeof statusArray[number]:これはマップされた型です。statusArrayのすべてのインデックスを反復処理し、それらのリテラル型を抽出します。numberインデックスシグネチャは、基本的に「この配列内の任意の要素の型を教えてください」と述べています。結果はunion type:"PENDING" | "PROCESSING" | "COMPLETED"です。
このアプローチは、文字列enumと同様の型安全性を提供しますが、生成されるJavaScriptは最小限です。statusArray自体は、JavaScriptでは文字列の配列のままです。
オブジェクトでのConst Assertions
Const assertionsは、オブジェクトに適用するとさらに強力になります。名前付き定数を表すキーと、リテラル文字列または数値である値を持つオブジェクトを定義できます。
const userRoles = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER"
} as const;
type UserRole = typeof userRoles[keyof typeof userRoles];
let currentUserRole: UserRole = "EDITOR";
// currentUserRole = "GUEST"; // Error: Type '"GUEST"' is not assignable to type 'UserRole'.
function displayRole(role: UserRole) {
console.log(`User role is: ${role}`);
}
displayRole(userRoles.Admin); // Valid
displayRole("EDITOR"); // Valid
このオブジェクトの例では、
as const:このassertionは、オブジェクト全体を読み取り専用にします。さらに重要なことは、すべてのプロパティ値(たとえば、stringの代わりに"ADMIN")に対してリテラル型を推論し、プロパティ自体をreadonlyにします。keyof typeof userRoles:この式は、userRolesオブジェクトのキーのunionである"Admin" | "Editor" | "Viewer"になります。typeof userRoles[keyof typeof userRoles]:これはルックアップ型です。キーのunionを取得し、それを使用してuserRoles型で対応する値を検索します。これにより、値のunion:"ADMIN" | "EDITOR" | "VIEWER"になり、これはロールに必要な型です。
userRolesのJavaScript出力は、プレーンなJavaScriptオブジェクトになります。
var userRoles = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER"
};
これは、一般的なenumよりも大幅に軽量です。
Const Assertionsを使用する場合
- 読み取り専用定数:実行時に変更しない固定された文字列または数値リテラルのセットが必要な場合。
- 最小限のJavaScript出力:バンドルサイズを気にし、定数の最もパフォーマンスの高いランタイム表現が必要な場合。
- オブジェクトのような構造:データまたは構成を構造化する方法と同様に、キーと値のペアの読みやすさを好む場合。
- 文字列ベースのセット:記述的な文字列で最もよく識別される状態、型、またはカテゴリを表す場合に特に役立ちます。
代替案2:Union Types
Union typesを使用すると、変数が複数の型のうちの1つの値を持つことができることを宣言できます。リテラル型(文字列、数値、ブールリテラル)と組み合わせると、セット自体の明示的な定数宣言を必要とせずに、許可された値のセットを定義する強力な方法を形成します。
文字列リテラルを持つUnion Types
文字列リテラルのunionを直接定義できます。
type TrafficLightColor = "RED" | "YELLOW" | "GREEN";
let currentLight: TrafficLightColor = "YELLOW";
// currentLight = "BLUE"; // Error: Type '"BLUE"' is not assignable to type 'TrafficLightColor'.
function changeLight(color: TrafficLightColor) {
console.log(`Changing light to: ${color}`);
}
changeLight("RED");
// changeLight("REDDY"); // Error
これは、許可された文字列値のセットを定義するための最も直接的で、多くの場合最も簡潔な方法です。
数値リテラルを持つUnion Types
同様に、数値リテラルを使用できます。
type HttpStatusCode = 200 | 400 | 404 | 500;
let responseCode: HttpStatusCode = 404;
// responseCode = 201; // Error: Type '201' is not assignable to type 'HttpStatusCode'.
function handleResponse(code: HttpStatusCode) {
if (code === 200) {
console.log("Success!");
} else {
console.log(`Error code: ${code}`);
}
}
handleResponse(500);
Union Typesを使用する場合
- シンプルで直接的なセット:許可された値のセットが小さく、明確で、値自体を超えて記述的なキーを必要としない場合。
- 暗黙的な定数:セット自体に名前付き定数を参照する必要はなく、リテラル値を直接使用する場合。
- 最大の簡潔さ:専用のオブジェクトまたは配列を定義することが大げさに感じるような、単純なシナリオの場合。
- 関数パラメーター/戻り値の型:関数の正確な許容される文字列または数値の入力/出力を定義するのに最適です。
Enums、Const Assertions、およびUnion Typesの比較
主な違いと使用例をまとめましょう。
ランタイムの動作
- Enums:JavaScriptオブジェクトを生成し、潜在的に逆マッピングを行います。
- Const Assertions(配列/オブジェクト):プレーンなJavaScript配列またはオブジェクトを生成します。型情報は実行時に消去されますが、データ構造は残ります。
- Union Types(リテラル):union自体にはランタイム表現がありません。値は単なるリテラルです。型のチェックは完全にコンパイル時に行われます。
読みやすさと表現力
- Enums:高い読みやすさ、特に記述的な名前を使用した場合。より冗長になる可能性があります。
- Const Assertions(オブジェクト):キーと値のペアによる優れた読みやすさ。構成または設定を模倣します。
- Const Assertions(配列):名前付き定数を表す場合は読みやすさが低く、値の順序付きリストの場合には適しています。
- Union Types:非常に簡潔。読みやすさは、リテラル値自体の明確さによって異なります。
型安全性
- 3つのアプローチすべてが、強力な型安全性を提供します。有効で事前定義された値のみを変数に割り当てるか、関数に渡すことができます。
バンドルサイズ
- Enums:一般的に、生成されたJavaScriptオブジェクトが原因で最大になります。
- Const Assertions:enumより小さく、プレーンなデータ構造を生成します。
- Union Types:型自体の特定のランタイムデータ構造を生成せず、リテラル値のみに依存するため、最小です。
使用例マトリックス
簡単なガイドを次に示します。
| 機能 | TypeScript Enum | Const Assertion(オブジェクト) | Const Assertion(配列) | Union Type(リテラル) |
|---|---|---|---|---|
| ランタイム出力 | JSオブジェクト(逆マッピングあり) | プレーンJSオブジェクト | プレーンJS配列 | なし(リテラル値のみ) |
| 読みやすさ(名前付き定数) | 高い | 高い | 中 | 低い(値が名前です) |
| バンドルサイズ | 最大 | 中 | 中 | 最小 |
| 柔軟性 | 良い | 良い | 良い | 優れている(単純なセットの場合) |
| 一般的な使用例 | 状態、ステータスコード、カテゴリ | 構成、ロール定義、機能フラグ | 不変値の順序付けられたリスト | 関数パラメーター、単純な制限値 |
実践的な例とベストプラクティス
例1:APIステータスコードの表現
Enum:
enum ApiStatus {
Success = "SUCCESS",
Error = "ERROR",
Pending = "PENDING"
}
function handleApiResponse(status: ApiStatus) {
// ... logic ...
}
Const Assertion(オブジェクト):
const apiStatusCodes = {
SUCCESS: "SUCCESS",
ERROR: "ERROR",
PENDING: "PENDING"
} as const;
type ApiStatus = typeof apiStatusCodes[keyof typeof apiStatusCodes];
function handleApiResponse(status: ApiStatus) {
// ... logic ...
}
Union Type:
type ApiStatus = "SUCCESS" | "ERROR" | "PENDING";
function handleApiResponse(status: ApiStatus) {
// ... logic ...
}
推奨事項:このシナリオでは、union typeが最も簡潔で効率的であることがよくあります。リテラル値自体で十分に説明的です。各ステータスに関連する追加のメタデータ(たとえば、ユーザーフレンドリーなメッセージ)が必要な場合は、const assertionオブジェクトの方が適しています。
例2:ユーザーロールの定義
Enum:
enum UserRoleEnum {
Admin = "ADMIN",
Moderator = "MODERATOR",
User = "USER"
}
function getUserPermissions(role: UserRoleEnum) {
// ... logic ...
}
Const Assertion(オブジェクト):
const userRolesObject = {
Admin: "ADMIN",
Moderator: "MODERATOR",
User: "USER"
} as const;
type UserRole = typeof userRolesObject[keyof typeof userRolesObject];
function getUserPermissions(role: UserRole) {
// ... logic ...
}
Union Type:
type UserRole = "ADMIN" | "MODERATOR" | "USER";
function getUserPermissions(role: UserRole) {
// ... logic ...
}
推奨事項:ここで、const assertionオブジェクトが適切なバランスを取っています。明確なキーと値のペア(例:userRolesObject.Admin)を提供し、ロールを参照するときの読みやすさを向上させながら、パフォーマンスも維持できます。直接的な文字列リテラルで十分であれば、union typeも非常に強力な候補です。
例3:構成オプションの表現
さまざまなテーマを持つ可能性のあるグローバルアプリケーションの構成オブジェクトを想像してください。
Enum:
enum Theme {
Light = "light",
Dark = "dark",
System = "system"
}
interface AppConfig {
theme: Theme;
// ... other config options ...
}
Const Assertion(オブジェクト):
const themes = {
Light: "light",
Dark: "dark",
System: "system"
} as const;
type Theme = typeof themes[keyof typeof themes];
interface AppConfig {
theme: Theme;
// ... other config options ...
}
Union Type:
type Theme = "light" | "dark" | "system";
interface AppConfig {
theme: Theme;
// ... other config options ...
}
推奨事項:テーマなどの構成設定の場合、const assertionオブジェクトが理想的であることがよくあります。利用可能なオプションとその対応する文字列値を明確に定義します。キー(Light、Dark、System)は説明的で、値に直接マッピングされるため、構成コードは非常に理解しやすくなっています。
適切なツールを選択する
TypeScript enums、const assertions、およびunion types間の決定は、常に白黒ではありません。多くの場合、ランタイムパフォーマンス、バンドルサイズ、およびコードの読みやすさ/表現力のトレードオフになります。
- 単純で制約された文字列または数値リテラルのセットが必要で、最大の簡潔さが必要な場合は、Union Typesを選択してください。これらは、関数シグネチャと基本的な値の制限に優れています。
- enumに似ていますが、ランタイムのオーバーヘッドが大幅に少ない、名前付き定数を定義するためのより構造化された読みやすい方法が必要な場合は、Const Assertions(Objectsを使用)を選択してください。これは、構成、ロール、またはキーが重要な意味を追加するセットに最適です。
- 不変値の順序付けられたリストが単純に必要で、名前付きキーよりもインデックスによる直接アクセスの方が重要な場合は、Const Assertions(Arraysを使用)を選択してください。
- 逆マッピングなどの特定の機能が必要な場合(ただし、これは最新の開発ではあまり一般的ではありません)、またはチームに強い好みがあり、プロジェクトに対するパフォーマンスへの影響が無視できる場合は、TypeScript Enumsを検討してください。
多くの最新のTypeScriptプロジェクトでは、特に文字列ベースの定数について、パフォーマンス特性が向上し、JavaScript出力がより単純であるため、従来のenumよりもconst assertionsとunion typesに傾いています。
グローバルな考慮事項
グローバルなオーディエンス向けにアプリケーションを開発する場合、一貫性があり予測可能な定数定義が重要です。これまでに説明した選択肢(enums、const assertions、union types)はすべて、さまざまな環境や開発者のロケールで型安全性を適用することにより、この一貫性に貢献しています。
- 一貫性:選択した方法に関係なく、重要なのはプロジェクト内での一貫性です。ロールにconst assertionオブジェクトを使用する場合は、コードベース全体でそのパターンを遵守してください。
- 国際化(i18n):国際化されるラベルまたはメッセージを定義するときは、これらの型安全な構造を使用して、有効なキーまたは識別子のみが使用されるようにします。実際の翻訳された文字列は、i18nライブラリを介して個別に管理されます。たとえば、"PENDING"、"PROCESSING"、"COMPLETED"にできる`status`フィールドがある場合、i18nライブラリはこれらの内部識別子をローカライズされた表示テキストにマッピングします。
- タイムゾーンと通貨:enumには直接関係ありませんが、日付、時刻、通貨などの値を扱う場合、TypeScriptの型システムは正しい使用を強制するのに役立ちますが、正確なグローバル処理には通常、外部ライブラリが必要です。たとえば、`Currency`union typeは`"USD" | "EUR" | "GBP"`として定義できますが、実際の変換ロジックには特別なツールが必要です。
結論
TypeScriptは、定数を管理するための豊富なツールセットを提供します。enumsは役立ちましたが、const assertionsとunion typesは、魅力的な、多くの場合、よりパフォーマンスの高い代替手段を提供します。パフォーマンス、読みやすさ、簡潔さなど、特定のニーズに基づいて、それらの違いを理解し、適切なアプローチを選択することにより、グローバルにスケーリングする、より堅牢で、保守性が高く、効率的なTypeScriptコードを作成できます。
これらの代替手段を採用すると、バンドルサイズの縮小、アプリケーションの高速化、国際的なチームにとってより予測可能な開発者エクスペリエンスにつながる可能性があります。